/*
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 *
 * The Initial Developer of the Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
 * Portions created by Mark A. Kobold are Copyright (C) 2000-2007. All Rights Reserved.
 *
 * Contributor(s): 
 *  Mark A. Kobold [mkobold <at> isqlviewer <dot> com].
 *  
 * If you didn't download this code from the following link, you should check
 * if you aren't using an obsolete version: http://www.isqlviewer.com
 */
package org.isqlviewer.sql;

import java.io.File;
import java.io.PrintWriter;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.log4j.FileAppender;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.Priority;
import org.isqlviewer.JdbcCommandLogger;
import org.isqlviewer.util.IsqlToolkit;
import org.isqlviewer.util.LocalMessages;
import org.isqlviewer.util.StringUtilities;

/**
 * Class that represents the connection to the database provider.
 * <p>
 * 
 * @author Mark A. Kobold &lt;mkobold at isqlviewer dot com&gt;
 * @version 1.0
 */
public final class JdbcService implements DataSource {

    // ResourceBundle identifier for providing natural language errors and other messages.
    private static final String RESOURCE_BUNDLE = "org.isqlviewer.sql.ResourceBundle";
    // JDBC environment variable for specifying the user-name to the connection.
    private static final String JDBC_ENV_PRINCIPAL = "user";
    // JDBC environment variable for specifying the user-password to the connection.
    private static final String JDBC_ENV_PASSWORD = "password";

    // detection variable when the connection parameters are modified after a connection is made.
    private transient int modCount = 0;

    // JDBC URL as the connect string the underlying connection.
    private String url = null;
    // Username for authenticating against the underlying connection.
    private String principal = null;
    // Password for the user-name when authenticating against the underlying connnection.
    private String credentials = null;
    // Fully qualified Java class name of the actual driver to use.
    private String driverClass = null;
    // User defined description for showing in tools to describe this connection.
    private String description = null;
    // Set of platform specific configuration parameters for tailoring a JDBC connection correctly.
    private Properties environment = new Properties();
    // Number of seconds that a connection is allowed to have when creating a connection.
    private int loginTimeout = 0;
    // Internal logging facility provided by log4j.
    private Logger logger = IsqlToolkit.getApplicationLogger();
    // User defined name for identifying this service.
    private String name = null;
    //
    private ServiceType type = null;

    // profile settings that determine certain behaviors for this service.
    private ConnectionProfile profile = null;

    private ConnectionWrapper cachedConnection = null;

    private JdbcCommandLogger commandLogger = null;

    // localization component for providing natural language errors and messages within this class.
    private static final LocalMessages messages = new LocalMessages(RESOURCE_BUNDLE, JdbcService.class);

    public JdbcService() {

        this(null);
    }

    public JdbcService(JdbcCommandLogger commandLogger) {

        this.commandLogger = commandLogger;
    }

    public Connection getConnection() throws SQLException {

        return getConnection(principal, credentials);
    }

    public Connection getConnection(String username, String password) throws SQLException {

        return createDriverConnection(username, password);
    }

    public PrintWriter getLogWriter() throws SQLException {

        return null;
    }

    public void setLogWriter(PrintWriter writer) throws SQLException {

        throw new SQLException();
    }

    public synchronized void setLoginTimeout(int loginTimeout) throws SQLException {

        this.loginTimeout = loginTimeout;
        modCount++;
    }

    public synchronized int getLoginTimeout() throws SQLException {

        return loginTimeout;
    }

    /**
     * Get the user credentials/password for this connection.
     * <p>
     * 
     * @return password for the user-name with this connection.
     */
    public synchronized String getCredentials() {

        return credentials;
    }

    /**
     * Sets the credentials/password for the user of this connection.
     * <p>
     * 
     * @param credentials userpassword for this connection.
     */
    public synchronized void setCredentials(String credentials) {

        this.credentials = credentials;
        modCount++;
    }

    /**
     * Get the fully qualified driver class name for this connection.
     * <p>
     * 
     * @return Java class name of the driver connection to the database.
     */
    public synchronized String getDriverClass() {

        return driverClass;
    }

    /**
     * Gets the user-defined description for this service.
     * <p>
     * Purpose of the description is to allow a short text to describe this service as deemed by the user.
     * 
     * @return user-defined description of this service.
     */
    public synchronized String getDescription() {

        return description;
    }

    /**
     * Sets the description for this service.
     * <p>
     * Intent is to allow the user to define a short text regarding the purpose of this message.
     * 
     * @param description to set for this service.
     */
    public synchronized void setDescription(String description) {

        this.description = description;
        modCount++;
    }

    /**
     * Sets the driver class for creating JDBC connections.
     * <p>
     * 
     * @param driverClass fully-qualified Java class name of the JDBC driver.
     */
    public synchronized void setDriverClass(String driverClass) {

        this.driverClass = driverClass;
        modCount++;
    }

    /**
     * Gets the current user-name of this connection.
     * <p>
     * 
     * @return user-name that is used to authenticate this connection.
     */
    public synchronized String getPrincipal() {

        return principal;
    }

    /**
     * Set the user-name to authenticate this connection with.
     * <p>
     * 
     * @param principal the user-name to use when authenticating this connection.
     */
    public synchronized void setPrincipal(String principal) {

        this.principal = principal;
        modCount++;
    }

    /**
     * Get the JDBC URL or connect string for this connection.
     * <p>
     * 
     * @return the JDBC URL for this connection.
     */
    public synchronized String getUrl() {

        return url;
    }

    /**
     * Set the JDBC URL or connect string for this connection.
     * <p>
     * Please refer to the JDBC Driver instructions for populating this field.
     * 
     * @param url for the driver to use when creating a new connection.
     */
    public synchronized void setUrl(String url) {

        this.url = url;
        modCount++;
    }

    /**
     * Get the connection environment for this connection.
     * <p>
     * This environment will contain the platform-specific configuration options for the JDBC driver to use when
     * creating the connection.
     * <p>
     * Please refer to the JDBC Driver instructions for configuring driver properties.
     * 
     * @return driver properties that can tune platform specific options for this connection.
     */
    public synchronized Properties getEnvironment() {

        return (Properties) environment.clone();
    }

    /**
     * @param environment The environment to set.
     */
    public synchronized void setEnvironment(Properties environment) {

        synchronized (this.environment) {
            this.environment.clear();
            this.environment.putAll(environment);
        }
        modCount++;
    }

    /**
     * Gets the connection profile used by this connection.
     * <p>
     * 
     * @return reference to the profile being used by this profile.
     */
    public synchronized ConnectionProfile getProfile() {

        return profile;
    }

    /**
     * Sets the profile for this service.
     * <p>
     * 
     * @param profile
     */
    public synchronized void setProfile(ConnectionProfile profile) {

        if (profile == null) {
            throw new NullPointerException(messages.format("DataSource.cannot_use_null_connection_profile"));
        }
        this.profile = profile;
    }

    /**
     * Get the user-defined name for this service.
     * <p>
     * This name should identify this service to the end-user. This is the name that should be used when looking up this
     * service initially.
     * 
     * @return the conical name for this service.
     */
    public synchronized String getName() {

        return name;
    }

    /**
     * Sets the conical name for this service.
     * <p>
     * 
     * @param name for this service so that it can be accessed by name for the end-user.
     */
    public synchronized void setName(String name) {

        this.name = name;
        modCount++;
    }

    /**
     * Sets the type of service this instance represents.
     * <p>
     * This parameter can be a bit confusing a iSQL-Viewer shared service is a service that is generally read-only and
     * is loaded across the network from a shared location such as a intranet web server or even the local file system.
     * <p>
     * A private service is readable and writable by the user that created it, most cases the private services can also
     * be a shared service for someone else depending on configurations.
     * 
     * @param type the type of service to set this service as.
     */
    public void setType(ServiceType type) {

        if (type == null) {
            throw new NullPointerException(ServiceType.class.getName());
        }
        this.type = type;
    }

    /**
     * Gets the type of service this instance represents.
     * <p>
     * 
     * @return the type of service this instance is.
     */
    public ServiceType getType() {

        return type;
    }

    public Driver getDriver() {

        return AccessController.doPrivileged(new PrivilegedAction<Driver>() {

            public Driver run() {

                ClassLoader classLoader = profile.toClassLoader();
                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(classLoader);
                try {
                    Class clazz = Class.forName(getDriverClass(), true, profile.toClassLoader());
                    return (Driver) clazz.newInstance();
                } catch (Throwable t) {
                    // fall-back on the driver manager to get the connection.
                    error(messages.format("DataSource.DriverError"), t);
                    return null;
                } finally {
                    Thread.currentThread().setContextClassLoader(contextClassLoader);
                }
            }
        });
    }

    /**
     * Logs informational error and supplementary message.
     * <p>
     * if informational messages are enabled the message and exception will be logged to designated destination.
     * 
     * @param message informational message to log.
     * @param error exception associated with the message.
     */
    void info(Object message, Throwable error) {

        if (logger.isInfoEnabled()) {
            logger.info(message, error);
        }
    }

    /**
     * Logs an informational message.
     * <p>
     * if informational loggins is enabled the message will be logged to designated destination.
     * 
     * @param message informational message to log.
     */
    void info(Object message) {

        if (logger.isInfoEnabled()) {
            logger.info(message);
        }
    }

    /**
     * Logs debugging messages and supplementary exception.
     * <p>
     * if debug messages are enabled the message and exception will be logged to designated destination.
     * 
     * @param message trace message to log.
     * @param error exception associated with the message.
     */
    void trace(Object message, Throwable error) {

        if (logger.isDebugEnabled()) {
            logger.debug(message, error);
        }
    }

    /**
     * Logs message for debugging purposes.
     * <p>
     * if debugging is enabled the message will be logged to designated destination.
     * 
     * @param message trace information to log.
     */
    void trace(Object message) {

        if (logger.isDebugEnabled()) {
            logger.debug(message);
        }
    }

    /**
     * Logs error messages and supplementary exception.
     * <p>
     * if error messages are enabled the message and exception will be logged to designated destination.
     * 
     * @param message error message to log.
     * @param error exception associated with the message.
     */
    void error(Object message, Throwable error) {

        logger.error(message, error);
    }

    /**
     * Logs an error message.
     * <p>
     * if error logging is enabled the message will be logged to designated destination.
     * 
     * @param message error message to log.
     */
    void error(Object message) {

        logger.error(message);
    }

    /**
     * Logs warning messages and supplementary exception.
     * <p>
     * 
     * @param message warning message to log.
     * @param error exception associated with the message.
     */
    void warn(Object message, Throwable error) {

        logger.warn(message, error);
    }

    /**
     * Logs warning messages.
     * <p>
     * 
     * @param message warning information to log.
     */
    void warn(Object message) {

        logger.warn(message);
    }

    /**
     * Build a connection to the database using the appropriate database driver.
     * <p>
     * 
     * @see DriverManager#getConnection(java.lang.String, java.util.Properties)
     * @return a connection to the database
     */
    Connection createDriverConnection(final String username, final String password) throws SQLException {

        if (cachedConnection != null) {
            return cachedConnection;
        }

        initializeLogging();
        Object result = AccessController.doPrivileged(new PrivilegedAction<Object>() {

            public Object run() {

                final String jdbcURL = getUrl();
                final Properties env = (Properties) environment.clone();
                env.setProperty(JDBC_ENV_PRINCIPAL, username == null ? "" : username);
                env.setProperty(JDBC_ENV_PASSWORD, password == null ? "" : password);
                ClassLoader classLoader = profile.toClassLoader();
                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                Thread.currentThread().setContextClassLoader(classLoader);
                try {
                    Class clazz = Class.forName(getDriverClass(), true, profile.toClassLoader());
                    final ArrayList<Object> resultPlaceHolder = new ArrayList<Object>();
                    final Driver driver = (Driver) clazz.newInstance();
                    long timeout = loginTimeout * 1000;
                    final Thread t = new Thread(new Runnable() {

                        public void run() {

                            try {
                                Connection connection = driver.connect(jdbcURL, env);
                                if (connection != null) {
                                    resultPlaceHolder.add(connection);
                                } else {
                                    throw new RuntimeException("Failed to connect to:'" + jdbcURL + "'");
                                }
                            } catch (SQLException sqle) {
                                error(messages.format("DataSource.driver_error"), sqle);
                                resultPlaceHolder.add(sqle);
                            } catch (Throwable e) {
                                // fall-back on the driver manager to get the connection.
                                error(messages.format("DataSource.driver_error"), e);
                                try {
                                    resultPlaceHolder.add(DriverManager.getConnection(jdbcURL, env));
                                } catch (Throwable error) {
                                    error(messages.format("DataSource.driver_error"), error);
                                    resultPlaceHolder.add(error);
                                }
                            }
                        }
                    });

                    t.setName("privileged:iSQL-Viewer/createDriverConnection()");
                    t.start();
                    synchronized (t) {
                        t.join(timeout);
                    }

                    if (!resultPlaceHolder.isEmpty()) {
                        return resultPlaceHolder.get(0);
                    }

                    t.interrupt();
                    String time = StringUtilities.getFullHumanReadableTime(timeout);
                    return new SQLException(messages.format("DataSource.connection_timed_out", time));
                } catch (InterruptedException ie) {
                    SQLException sqle = new SQLException(messages.format("DataSource.connection_interrupted"));
                    return sqle;
                } catch (Throwable t) {
                    // fall-back on the driver manager to get the connection.
                    error(messages.format("DataSource.DriverError"), t);
                    try {
                        return DriverManager.getConnection(jdbcURL, env);
                    } catch (Throwable error) {
                        error(messages.format("DataSource.DriverError"), error);
                        return error;
                    }
                } finally {
                    Thread.currentThread().setContextClassLoader(contextClassLoader);
                }
            }
        });

        if (result instanceof SQLException) {
            throw (SQLException) result;
        } else if (result instanceof Throwable) {
            Throwable error = (Throwable) result;
            throw new SQLException(error.getMessage());
        } else if (result instanceof Connection) {
            cachedConnection = new ConnectionWrapper(this, (Connection) result);
            RegistrarFactory registrarFactory = RegistrarFactory.getSharedInstance();
            PlatformRegistrar registrar = registrarFactory.lookupRegistrar(getDriverClass());
            if (registrar != null) {
                boolean registered = false;
                try {
                    registered = registrar.register(cachedConnection.getConnection());
                    if (registered) {
                        info(messages.format("DataSource.platform_registration_sucessfull", getName()));
                    } else {
                        warn(messages.format("DataSource.failed_to_register_correctly", getName()));
                    }
                } catch (SQLException sqle) {
                    error(messages.format("DataSource.platform_registration_failed", result), sqle);
                }
            }
            return cachedConnection;
        }
        throw new RuntimeException(messages.format("DataSource.connectionerror", result));
    }

    synchronized void clearConnection() {

        cachedConnection = null;
        Logger newLogger = Logger.getLogger(getDriverClass());
        newLogger.removeAllAppenders();
        logger = IsqlToolkit.getApplicationLogger();
        System.gc();
    }

    /**
     * Gets the current command logger for logging activity from this service.
     * <p>
     * 
     * @return <tt>null</tt> if no command logger is available.
     */
    public JdbcCommandLogger getCommandLogger() {

        return commandLogger;
    }

    /**
     * @param commandHistory
     */
    public void setCommandLogger(JdbcCommandLogger commandLogger) {

        this.commandLogger = commandLogger;
    }

    private void initializeLogging() {

        // we will use the driver class package name instead of our own since the driver
        // is likely to use a logger in this category if it also uses log4j
        // TODO Add hook for java logging as well //
        Object[] paths = driverClass.split("\\.");
        String packageName = MessageFormat.format("{0}.{1}", paths);
        Logger newLogger = Logger.getLogger(packageName);
        newLogger.setAdditivity(true);
        newLogger.removeAllAppenders();
        PatternLayout patternLayout = new PatternLayout("%d{ISO8601} [%-5p] - {%t} - %m\n");
        FileAppender fileAppender = new FileAppender();
        fileAppender.setLayout(patternLayout);
        File logFile = new File(IsqlToolkit.getLoggingDirectory(), getName().concat(".log"));
        fileAppender.setFile(logFile.getAbsolutePath());
        fileAppender.setAppend(false);
        // TODO make this go to DEBUG externally if user wants it too //
        fileAppender.setThreshold(Priority.INFO);
        fileAppender.activateOptions();
        newLogger.addAppender(fileAppender);
        logger = newLogger;
    }
}
